// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_DOWNLOADER_HPP_
#define SCRW_DOWNLOADER_HPP_

#include <set>
#include <libv/check.h>

#include "curl.h"
#include "manager.h"
#include "common.h"
#include "url_converter.h"


namespace scrw
{

/** Непотокобезопасный объект, выполняющий задания загрузки документов,
  * получаемые от создавшего его менеджера. Загружаются не все документы,
  * а только имеющие поддерживаемый тип: plain text, html, xhtml, rss и atom.
  * Если файл имеет другой тип, загрузка прерывается, а менеджеру
  * отправляется соответствующее сообщение.
  * Для низкоуровневой настройки загрузки (например, для задания адреса
  * proxy-сервера) следует использовать метод curl_handle().low_level_handle(),
  * возвращающий объект CURL* библиотеки curl.
  *
  * Создание объекта осуществляется методом Manager::GetDownloader().
  *
  * Класс не поддерживает присваивание, так как агегирует объекты с данным
  * ограничением, однако, разрешено копирование. Таким образом, возможно
  * создание объекта в куче и использование его через указатель.
  *
  * Под непотокобезопасностью подразумевается запрет на совместное использование
  * одного объекта несколькими потоками. Разрешено создание нескольких объектов
  * для одновременной работы в нескольких потоках.
  *
  * Пример использования:
  * <code>
  * Manager m(...);
  *
  * // Создание в нескольких потоках параллельно работающих объектов.
  * Manager::Downloader d = m.GetDownloader();
  *
  * // Создание объекта в куче (не используется в примере).
  * Manager::Downloader* pd = new Manager::Downloader(m.GetDownloader());
  *
  * // Выполнение заданий пока они есть. Если метод вернул false — задания
  * // закончились, но возможно появятся, если есть еще страницы,
  * // из которых не были выделены URL.
  * while ( d.TryDoOneJob() ) continue;
  * </code> */
class Manager::Downloader
{
  friend class Manager;  // только менеджер может создавать данный объект.
  public:
    /** Попытка получить и выполнить новое задание менеджера.
      * @retval true получено 1 задание, результат отправлен менеджеру
      * @retval false очередь заданий менеджера пуста.
      * Метод не потокобезопасен и не генерирует исключения. */
    bool TryDoOneJob()
    {
        // Всегда забираем задачу, так как это единственный способ атомарно
        // проверить их наличие (через возврат пустой задачи) и получить новую.
        Manager::DownloadJob job = manager_->PopDownloadJob();
        if ( job.depth == -1 )
            return false;


        // Downloader не использует parent_url,
        // но чем раньше поймать некорректные данные, тем лучше.
        if ( job.url.isBogus() || job.parent_url.isBogus() )
        {
            manager_->PushFailedDownloadJob(job, "url is bogus");
            return true;
        }

        job.data.clear();
        job.mime_type.clear();
        job.charset.clear();


        Curl::Handle::PerformResult r;
        try
        {
            r = curl_handle_.Perform(
                url_converter_.Unicode2Ascii(job.url), FileTypeFilter());
        }
        catch(UrlConverterException& e)
        {
            manager_->PushFailedDownloadJob(job, e.what());
            return true;
        }
        catch(FileSkippedException&)  // перед CurlException
        {
            manager_->PushAbortedDownloadJob(job, "Unsupported mime type");
            return true;
        }
        catch(CurlException& e)
        {
            manager_->PushFailedDownloadJob(job, e.what());
            return true;
        }
        catch(libv::PrecondException& e)  // На всякий случай.
        {
            manager_->PushFailedDownloadJob(job, e.what());
            return true;
        }
        catch(::std::exception& e)  // На всякий случай.
        {
            ERR("Downloader caught unexpected exception: " << e.what());
            manager_->PushFailedDownloadJob(job, e.what());
            return true;
        }

        job.data.swap(r.data);  // оптимизация, без копирования.
        job.mime_type = r.mime_type;
        job.charset = r.charset;

        manager_->PushDownloadJobResult(job);

        return true;
    }


    /** Метод возвращает внутренний хэндл,
      * что позволяет более точно настроить работу объекта.
      * Метод не потокобезопасен и не генерирует исключения. */
    Curl::Handle& curl_handle()
    {
        return curl_handle_;
    }


  private:
    /** Присваивание запрещено. */
    Downloader& operator=(const Downloader&);

    /** Функтор, передаваемый методу Curl::Handle::Perform().
      * По переданным ему параметрам документа определяет поддерживается
      * ли данный тип и следует ли продолжать загрузку.
      * @sa Curl::Handle::Perform. */
    class FileTypeFilter
    {
      public:
        enum { CONTINUE_DOWNLOAD = 1, NEED_MORE_DATA = 0, BREAK_DOWNLOAD = -1 };

        FileTypeFilter()
        {
            mime_types_.insert(MimeType());  // html может содержать тег meta.
            mime_types_.insert(MimeType("text/plain"));
            mime_types_.insert(MimeType("text/html"));
            mime_types_.insert(MimeType("text/xml"));
            mime_types_.insert(MimeType("application/xml"));
            mime_types_.insert(MimeType("application/atom+xml"));
            mime_types_.insert(MimeType("application/rdf+xml"));
            mime_types_.insert(MimeType("application/rss+xml"));
            mime_types_.insert(MimeType("application/xhtml+xml"));
        }


        int operator()(const AsciiUrl& /*url*/,
                       const MimeType& mime_type,
                       const Charset& /*charset*/,
                       const ::std::vector<char>& /*data*/) const
        {
            if ( mime_types_.find(mime_type) == mime_types_.end() )
                return BREAK_DOWNLOAD;

            return CONTINUE_DOWNLOAD;
        }

      private:
        ::std::set<MimeType> mime_types_;
    };


    Downloader(Manager* manager, const Curl::Handle& curl_handle)
        : manager_(manager),
          curl_handle_(curl_handle)
    {
        PRECOND(manager != NULL);
    }



    /** Указатель на менеджер, выдающий задания и принимающий результаты. */
    Manager* manager_;

    /** Объект, непосредственно производящий загрузку документов. */
    Curl::Handle curl_handle_;

    /** Преобразователь URL из Unicode в ASCII-форму. */
    UrlConverter url_converter_;
};

}  // scrw


#endif  // SCRW_DOWNLOADER_HPP_
